//
// Notes
//  - The divs in each cell are for 2 reasons:
//    To keep the bordering alive and to use for slideDown
//


// Retrieved from:
// http://spudly.shuoink.com/2008/05/31/arrayintersection/
// Complements to Stephen Sorensen
Array.prototype.intersection = function( setB ) {  
   var setA = this;  
  
   var setA_seen = {};  
   var setB_seen = {};  
   for ( var i = 0; i < setB.length; i++ ) {  
      setB_seen[ setB[i] ] = true;  
   }  
  
   var intersection = [];  
   for ( var i = 0; i < setA.length; i++ ) {  
      if ( !setA_seen[ setA[i] ] ) {  
         setA_seen[ setA[i] ] = true;  
         if ( setB_seen[ setA[i] ] ) {  
            intersection.push( setA[i] );  
         }  
      }  
   }  
   return intersection;  
};


var TAB         = 9;
var RETURN      = 13;
var ESCAPE      = 27;
var PAGE_UP     = 33;
var PAGE_DOWN   = 34;
var LEFT_ARROW  = 37;
var UP_ARROW    = 38;
var RIGHT_ARROW = 39;
var DOWN_ARROW  = 40;


function TimeNGears() {
    this.is_saved = true; // start off saved so can open without annoying dialog
    this.days_of_week = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];
    this.input_cells = ['tracCell', 'jobCell', 'monday','tuesday','wednesday','thursday','friday','saturday','sunday'];

    // notification
    this.notification_fadein_delay = 150;
    this.notification_display_delay = 1000;
    this.notification_fadeout_delay = 2000;

    this.db_fields = {
        category:    'jobCategoryCell',
        trac_number: 'tracCell',
        description: 'jobCell',
        monday:      'monday',
        tuesday:     'tuesday',
        wednesday:   'wednesday',
        thursday:    'thursday',
        friday:      'friday',
        saturday:    'saturday',
        sunday:      'sunday'
    };

    this.dialog_options = {
        fixedcenter: true,
        visible: true,
        modal: true,
        constraintoviewport: true,
        dragOnly: true
    };
};

TimeNGears.prototype.load = function() {

    // check if gears is installed
    this.checkGearsInstallation();

    // initialise the database if necessary
    this.initDb();

    // decorate the html with functionality
    this.decorate();

    // load a timesheet if one has been chosen as a default
    this.loadDefaultTimesheet();
}

TimeNGears.prototype.checkGearsInstallation = function() {
    if (!window.google || !google.gears) {
        location.href = "http://gears.google.com/?action=install&message=To run the timesheet please install Google Gears" +
                        "&return=" + location.href;
    }
};

TimeNGears.prototype.initDb = function() {

    // Open this page's local database.
    var success = false;
 
    if (window.google && google.gears) {
        try {
            this.db = google.gears.factory.create('beta.database');
 
            if (this.db) {
                this.db.open('timesheet');
                this.db.execute('create table if not exists timesheet' +
                           ' (timesheet_id integer primary key autoincrement, name varchar(255) unique)');
                this.db.execute('create table if not exists entry' +
                           ' (entry_id integer primary key autoincrement, timesheet_id integer, category integer, trac_number varchar(255), description varchar(255), monday_date date, monday real, tuesday real, wednesday real, thursday real, friday real, saturday real, sunday real)');
                this.db.execute('create table if not exists settings' +
                           ' (default_timesheet_id integer, issue_tracker varchar(255), issue_tracker_url varchar(255))');
                var rs = this.db.execute('select count(*) from settings');
                if (rs.field(0) == 0) {
                    this.db.execute('insert into settings values (null, "trac", null)');
                }
                rs.close();
                this.db.execute('create table if not exists category' +
                           ' (category_id integer primary key autoincrement, name varchar(255))');
 
                success = true;
            }
 
        } catch (ex) {
            alert('Could not create database: ' + ex.message);
        }
    }

    // Enable or disable UI elements
 
    //var inputs = document.forms[0].elements;
    //for (var i = 0, el; el = inputs[i]; i++) {
        //el.disabled = !success;
    //}
};

TimeNGears.prototype.decorate = function() {

    var that = this;

    this.loadJobCategories();

    // cancel the bubble which only seems to happen on a mac 
    $('td.inputCell input').click(function() { return false; });
    $('td.selectCell select').click(function() { return false; });

    // attach the cell editing functions
    $('td.inputCell, #timesheet_name_container').click(function(e){

        var obj = $(this);

        // if has nbsp then text returns a space
        var value_span = obj.find('span.value');
        var value = value_span.text();
        if (value.charCodeAt(0) == 160) {
            value = '';
        }

        if (e.shiftKey && value !== '' && obj.hasClass("tracCell")) {
            that.openIssueTracker(value);
            return false;
        }

        value_span.hide();
        obj.find('input').show().focus().val(value).select();
    });

    $('td.selectCell').click(function() {
        $(this).find('.display').hide();
        var value = $(this).find('.value').val();
        if (value.charCodeAt(0) == 160) {
            value = '';
        }
        $(this).find('select').show().focus().find('option[value="'+value+'"]').attr("selected", true);
    });

    $('#timesheet_name_container input').blur(function(){

        var span = $(this).parents('h2').eq(0).find('span.value');
        var value = $(this).val();
        var span_val = span.text();

        // unicode 160 is nbsp, not a ' '
        if (value !== '' && value !== span_val || value === '' && span_val.charCodeAt(0) !== 160) {
            that.setSaved(false);
        }

        if (this.value === '') {
            alert('Please enter a name');
            var value = $(this).parents('h2').eq(0).find('span.value').text();
            $(this).focus().val(value).select();
        } else {
            $(this).hide();
            $(this).parents('h2').eq(0).find('span.value').show().text(this.value);
        }
    });

    $('td.inputCell input').blur(function(){

        var span = $(this).parents('td').eq(0).find('span.value');
        var value = $(this).val();
        var span_val = span.text();

        // unicode 160 is nbsp, not a ' '
        if (value !== '' && value !== span_val || value === '' && span_val.charCodeAt(0) !== 160) {
            that.setSaved(false);
            $(this).parents('tr').attr('dirty', 1);
        }


        // hide the input
        $(this).hide();

        // if this is a template row, clone if entered something
        var parentTr = $(this).parents('tr').eq(0);
        if ($(this).val() !== '' && parentTr.hasClass('template')) {
            parentTr.clone(true).insertAfter(parentTr);  // create a new template row
            parentTr.removeClass('template').addClass('timesheetRow').find('input.delete').show();  // this is no longer the template row, and show the delete button
            var creating_new = true;
        } else {
            var creating_new = false;
        }

        if (this.value === '') {
            $(this).parents('td').eq(0).find('span.value').show().html('&nbsp;');
        } else {
            $(this).parents('td').eq(0).find('span.value').show().text(this.value);
        }

        // total the hours for this row
        if ($(this).parents('td').eq(0).hasClass('timeCell') && !parentTr.hasClass('template') || creating_new) {
            that.runCalculation($(this).parents('td').eq(0));
        }
    });

    $('td.selectCell select').blur(function() {

        var selected_option = $(this).find('option:selected');
        var value = selected_option.val();
        var hidden_val = $(this).siblings('input.value').val();

        // unicode 160 is nbsp, not a ' '
        if (value !== '' && value !== hidden_val || value === '' && hidden_val !== '') {
            that.setSaved(false);
            $(this).parents('tr').attr('dirty', 1);
        }

        $(this).hide();

        // if this is a template row, clone if entered something
        var parentTr = $(this).parents('tr').eq(0);
        if (value !== '' && parentTr.hasClass('template')) {
            parentTr.clone(true).insertAfter(parentTr);  // create a new template row
            parentTr.removeClass('template').addClass('timesheetRow').find('input.delete').show();  // this is no longer the template row, and show the delete button
        }

        if (value === '') {
            $(this).siblings('.display').show().html('&nbsp;');
        } else {
            $(this).siblings('.display').show().text(selected_option.text());
        }
        $(this).siblings('.value').val(value);
    });

    // capture some useful keys
    $('td.inputCell input, #timesheet_name_container input').keyup(function(event){
        var keycode = event.keyCode;

        // use escape to revert
        if (keycode == ESCAPE) {
            this.value = $(this).parents('td').eq(0).find('span.value').text();
            $(this).blur();
            return false;
        }

        // tie return key to blur
        if (keycode == RETURN) {

            // if the issue tracker cell, and the shift key is pressed take the user to the issue tracker
            if (event.shiftKey && $(this).parents('td').eq(0).hasClass('tracCell')) {
                that.openIssueTracker($(this).val());
                return false;
            }

            $(this).blur();
            return false;
        }

    }).
    // use the tab key to move across a cell - only with keydown event
    keydown(function(event) {

        var keycode = event.keyCode;

        if (keycode == TAB         ||
            keycode == PAGE_UP     ||
            keycode == PAGE_DOWN   ||
            keycode == LEFT_ARROW  ||
            keycode == UP_ARROW    ||
            keycode == RIGHT_ARROW ||
            keycode == DOWN_ARROW) {

            var obj = $(this);

            obj.blur();

            var week = obj.parents('.week').eq(0).attr("id");
            var row_index = obj.parents('tr').eq(0).prevAll('.timesheetRow').length;
            var curr_cell = that.whichCell(obj.parents('td').eq(0).attr('class'));

            switch(keycode) {

                case TAB:
                    if (event.shiftKey) {
                        curr_cell = that.previousCell(curr_cell);
                    } else {
                        curr_cell = that.nextCell(curr_cell);
                    }
                    break;

                case RIGHT_ARROW:
                    curr_cell = that.nextCell(curr_cell);
                    break;

                case LEFT_ARROW:
                    curr_cell = that.previousCell(curr_cell);
                    break;

                case UP_ARROW:
                    if (row_index > 0) {
                        row_index--;
                    }
                    break;

                case DOWN_ARROW:
                    row_index++;
                    break;

                case PAGE_UP:
                    week = $('.week#' + week).prev().attr("id");                    
                    break; 

                case PAGE_DOWN:
                    week = $('.week#' + week).next().attr("id");                    
                    break;
            }

            that.focus(week, row_index, curr_cell);

            // prevent the default by returning false
            return false;
        }
    });


    $('button').each(function() {
        var accesskey = $(this).attr('accesskey');
        var id = this.id;
        // replaces the button with a span and embeds the button further down
        new YAHOO.widget.Button(this);
        if (accesskey !== undefined) {
            $('#' + id + ' button').attr('accesskey', accesskey);
        }
    });

    $('#new').click(function(){that.newTimesheet()});
    $('#open').click(function(){that.open()});
    $('#save').click(function(){that.save()});
    $('#discard').click(function(){that.discard()});
    $('#monthlyReport').click(function(){that.monthlyReport()});
    $('#export').click(function(){that.exportTimesheet()});
    $('#options').click(function(){that.options()});
    $('#addWeek').click(function(){that.addWeek()});
    $('input.delete').click(function(){that.deleteRow(this)});


    // attach the endless scroll
    $(document).endlessScroll({
        bottomPixels: 10,
        callback: function() {that.loadMore()}
        //ceasefire: function() {return true}
    });
};

TimeNGears.prototype.loadJobCategories = function() {

    var select = $('select[name="job_category"]');
    select.empty();  // for when reloading
    var rs = this.db.execute('select * from category');
    select.append('<option value="">');
    while (rs.isValidRow()) {
        var option = $('<option/>');
        option.val(rs.fieldByName('category_id'));
        option.text(rs.fieldByName('name'));
        select.append(option);
        rs.next();
    }
    rs.close();
}

TimeNGears.prototype.loadDefaultTimesheet = function() {

    var rs = this.db.execute('select default_timesheet_id from settings');
    var default_timesheet_id = rs.field(0);
    if (default_timesheet_id !== null) {
        this.loadTimesheet(default_timesheet_id);
        this.setSaved(true);
    }
    rs.close();
}

TimeNGears.prototype.install = function() {
    var desktop = google.gears.factory.create('beta.desktop');
    desktop.createShortcut('time-n-gears',
                           location.href,
                           {'128x128': '',
                            '48x48': '',
                            '32x32': '',
                            '16x16': ''},
                           'Serverless browser-based timesheet application');
};

TimeNGears.prototype.monthlyReport = function() {

	var percentages = [];
	var labels = [];

	// 1. calculate the number of hours logged so far this month
	var now = new Date();
	var start_of_month = now.getFullYear() + (now.getMonth() + 1) + "01";
	var rs = this.db.execute("select sum(coalesce(monday,0) + coalesce(tuesday,0) + coalesce(wednesday,0) + coalesce(thursday,0) + coalesce(friday,0) + coalesce(saturday,0) + coalesce(sunday,0)) from entry where monday_date >= ?", [start_of_month]);
	var total_hours = rs.field(0);
	rs.close();

	// 2. calculate the percentages for each category
	var rs = this.db.execute('select ifnull(category.name,"Other") as label, sum(ifnull(monday,0) + ifnull(tuesday,0) + ifnull(wednesday,0) + ifnull(thursday,0) + ifnull(friday,0) + ifnull(saturday,0) + ifnull(sunday,0)) as category_total from entry left join category on category.category_id = entry.category where monday_date >= ? group by category order by category_total desc', [start_of_month]);
	while (rs.isValidRow()) {
		var percent = Math.round(10000 * rs.fieldByName("category_total") / total_hours) / 100;
		labels[labels.length] = rs.fieldByName("label") + " - " + percent + "%";
		percentages[percentages.length] = percent;
		rs.next();
	}
	rs.close();

	var chart_url = "http://chart.apis.google.com/chart?cht=p&chd=t:" + percentages.join(",") + "&chs=450x200&chl=" + labels.join("|") + "&chco=0000FF";

	var body = $('<img />').attr("src", chart_url);

	this.openDialog({
		title: "Monthly Report",
		html: body,
		options: {width: "500px", height: "300px"}
	});
}

TimeNGears.prototype.exportTimesheet = function() {

    // Load the rest of the timesheet
    while (this.loadMore());

    // Remove user interface
    $('body').children().not('#timesheet').hide();
    $('.deleteColumn').hide();
    $('.dateRangeCell').attr("colspan", "2");
};

TimeNGears.prototype.options = function() {

    var rs = this.db.execute("select * from category order by name"); 
    options = {};
    while (rs.isValidRow()) {
        options[rs.fieldByName("category_id")] = rs.fieldByName("name");
        rs.next();
    }
    rs.close();

    var rs = this.db.execute("select issue_tracker_url from settings");
    var issue_tracker_url = rs.field(0);
    rs.close();

    var that = this;
    this.openDialog({
        title:       "Options",
        html:        "options.html",
        js:          "options.js",
        ok_callback: function() {
            // save the jobs
            $('#dialogContainer select[name="job_category"] option').each(function() {
                if (parseInt(this.value) != this.value) {
                    that.db.execute("insert into category values (null, ?)", [this.value]);
                }
            });

            // remove deleted jobs
            $('#dialogContainer input.delete').each(function() {
                that.db.execute("delete from category where category_id = ?", [this.value]);
                that.db.execute("update entry set category = null where category = ?", [this.value]);
                $('.jobCategoryCell input.value[value="' + this.value + '"]').val("").siblings('.display').text("");
                $(this).remove();
            });

            // rename jobs
            $('#dialogContainer select[name="job_category"] option[dirty="1"]').each(function() {
                var new_text = $(this).text();
                that.db.execute("update category set name = ? where category_id = ?", [new_text, this.value]);
                $('.jobCategoryCell .value[value="' + this.value + '"]').siblings('.display').text(new_text);
            });

            // update the job lists
            that.loadJobCategories();

            // save the issue tracker url
            var issue_tracker_url = $('#issue_tracker_url').val();
            if (issue_tracker_url === '') {
                that.db.execute("update settings set issue_tracker_url = null");
            } else {
                that.db.execute("update settings set issue_tracker_url = ?", [issue_tracker_url]);
            }

        },
        select_options: {job_category: options},
        options: {width: "400px", height: "370px"}
    });

    $('#issue_tracker_url').val(issue_tracker_url);
    $('button#install').click(function(){that.install()});
};

TimeNGears.prototype.openDialog = function(dialog_arguments) {

    var dialog_script_id;

    var body;
    if (dialog_arguments.html.jquery !== undefined) {
    	body = dialog_arguments.html;
    } else {
	    body = $($.ajax({
	        url: dialog_arguments.html,
	        type: "GET",
	        dataType: "html",
	        async: false
	    }).responseText);
    }

    for (var name in dialog_arguments.select_options) {
        var select = $('select[name="' + name + '"]', body);
        for (var key in dialog_arguments.select_options[name]) {
            select.append($('<option>').val(key).text(dialog_arguments.select_options[name][key]));
        }
    }

    var ok_handler = function() {
        var callback = dialog_arguments.ok_callback;
        callback();
        hide_dialog();
    }

    var hide_dialog = function() {
        dialog.hide();
        if (dialog_script_id !== undefined) {
            $('#' + dialog_script_id).remove();
        }
        dialog.destroy();
    };

    var dialog = new YAHOO.widget.Dialog("dialog", $.extend(this.dialog_options, {}, {
        buttons: [
            { text: "OK", handler: ok_handler, isDefault: true },
            { text: "Cancel", handler: hide_dialog }
        ]
    }, dialog_arguments.options));
    dialog.setHeader(dialog_arguments.title);
    dialog.setBody(body.get(0));
    dialog.render("dialogContainer");
    dialog.focusFirst(); // not working?

    $('#dialogContainer .bd button').each(function() {
        var accesskey = $(this).attr('accesskey');
        var id = this.id;
        new YAHOO.widget.Button(this);
        if (accesskey !== undefined) {
            $('#' + id + ' button').attr('accesskey', accesskey);
        }
    });


    // events are not copied into the dialog, need to apply after rendering
    $('#dialogContainer input, #dialogContainer select').keyup(function(event) {
        var keycode = event.keyCode;
        // ESC
        if (keycode === ESCAPE) {
            hide_dialog();
        }
        if (keycode === RETURN) {
            ok_handler();
        }
    });

    $('#dialogContainer button').keyup(function(event) {
        if (event.keyCode === ESCAPE) {
            hide_dialog();
        }
    });

    //$('#dialogContainer select').dblclick(function() {
        //ok_handler();
    //});

    if (dialog_arguments.js !== undefined) {
        var js = $("<script>" + $.ajax({
            url: dialog_arguments.js,
            type: "GET",
            dataType: "javascript",
            async: false
        }).responseText + "</script>");

        var now = new Date;
        dialog_script_id = "dialog_script_" + now.getTime();
        js.attr("id", dialog_script_id);

        $('head').append(js);
    }

    if (body.hasClass("yui-navset")) {
    	tab_view = new YAHOO.widget.TabView(body.attr("id"));
    }
};

TimeNGears.prototype.newTimesheet = function() {

    if (!this.is_saved && !confirm('The current changes will be lost.\nAre you sure?')) {
        return;
    }
    window.location = window.location;
};

TimeNGears.prototype.open = function() {

    var that = this;

    // confirm with the user
    if (!this.is_saved && !confirm('The current changes will be lost.\nAre you sure?')) {
        return;
    }

    // setup a dialog to ask the user which timesheet

    var rs = this.db.execute('select timesheet_id, name from timesheet');
    var options = {};
    while (rs.isValidRow()) {
        options[rs.fieldByName('timesheet_id')] = rs.fieldByName('name');
        rs.next();
    }
    rs.close();

    this.openDialog({
        title: "Open Timesheet",
        html: "open.html",
        ok_callback: function() {
            var timesheet_id = $('#dialogContainer select[name="open_timesheet_id"] option:selected').val();
            if (timesheet_id === undefined) {
                return;
            }
            var open_at_startup = $('#dialogContainer input[name="open_at_startup"]').attr("checked");
            if (open_at_startup) {
                that.db.execute('update settings set default_timesheet_id = ?', [timesheet_id]);
            }
            that.loadTimesheet(timesheet_id);
            that.setSaved(true);
        },
        select_options: {open_timesheet_id: options}
    });
};

// responsibility: save given a timesheet name?
TimeNGears.prototype.save = function() {

    var that = this;

    // create or save the timesheet
    var timesheet_id = $('#timesheet_id').val();
    var timesheet_name = $('#timesheet_name').text();

    if (timesheet_id === '') {
        // how to get the id from here? catching the resultset then examining gives "resultset is closed"
        this.db.execute('insert into timesheet values(null, ?)', [timesheet_name]);
        timesheet_id = this.db.lastInsertRowId;
        $('#timesheet_id').val(timesheet_id);
    } else {
        this.db.execute('update timesheet set name = ? where timesheet_id = ?', [timesheet_name, timesheet_id]);
    }


    // save the dirty timesheet entries
    $('tr.timesheetRow[dirty="1"]').each(function(){

        var monday_date = $(this).parents('div').eq(0).attr('id');

        var id = $(this).attr('id');
        if (id === undefined || id === '') {
            var sql = 'insert into entry values(null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
        } else {
            var sql = 'update entry set category = ?, trac_number = ?, description = ?, monday_date = ?, monday = ?, tuesday = ?, wednesday = ?, thursday = ?, friday = ?, saturday = ?, sunday = ? where entry_id = ?';
        }
        var values = new Object;
        $(this).find('td.inputCell').find('span.value').each(function(){
            var parentTd = $(this).parents('td').eq(0);
            var key;
            if (parentTd.hasClass('tracCell')) {
                key = 'trac_number';
            } else if (parentTd.hasClass('jobCell')) {
                key = 'description';
            } else if (parentTd.hasClass('monday')) {
                key = 'monday';
            } else if (parentTd.hasClass('tuesday')) {
                key = 'tuesday';
            } else if (parentTd.hasClass('wednesday')) {
                key = 'wednesday';
            } else if (parentTd.hasClass('thursday')) {
                key = 'thursday';
            } else if (parentTd.hasClass('friday')) {
                key = 'friday';
            } else if (parentTd.hasClass('saturday')) {
                key = 'saturday';
            } else if (parentTd.hasClass('sunday')) {
                key = 'sunday';
            }
            values[key] = this.innerHTML === '&nbsp;' ? null : $(this).text();
        });

        values["category"] = $(this).find('td.jobCategoryCell select[name="job_category"] option:selected').val();

        if (id === undefined || id === '') {
            that.db.execute(sql, [timesheet_id, values['category'], values['trac_number'], values['description'], monday_date, values['monday'], values['tuesday'], values['wednesday'], values['thursday'], values['friday'], values['saturday'], values['sunday']]);

            // feed the newly created id back into the row
            $(this).attr('id', that.db.lastInsertRowId);

        } else {
            that.db.execute(sql, [values['category'], values['trac_number'], values['description'], monday_date, values['monday'], values['tuesday'], values['wednesday'], values['thursday'], values['friday'], values['saturday'], values['sunday'], id]);
        }
    });


    that.setSaved(true);
    $('tr.timesheetRow').attr('dirty', 0);
    that.showNotification('Timesheet saved');
}

TimeNGears.prototype.discard = function() {

    if (!confirm('Are you sure?')) {
        return;
    }

    // from here reload the currently opened timesheet
};

TimeNGears.prototype.addWeek = function() {

    var that = this;
    var new_date;

    var dialog = new YAHOO.widget.Dialog("chooseWeek", {
        width: "220px",
        height: "285px",
        fixedcenter: true,
        visible: true,
        modal: true,
        close: true,
        constraintoviewport: true,
        buttons: [ { text: "OK", handler: function() {
            var dates = cal.getSelectedDates();
            that.addWeekToTimesheet(dates[0]);
            this.hide(); // removes the modal mask
            $('div#dialogContainer').empty();
            }, isDefault: true }, { text: "Cancel", handler: function() {this.hide();$('div#dialogContainer').empty();} }  ]
    });
    dialog.setHeader("Add Week");
    dialog.setBody('<div id="dialogInner"></div>');
    dialog.render("dialogContainer");

    var cal = new YAHOO.widget.Calendar("cal", "dialogInner");
    cal.select(new Date);
    cal.render();
};

TimeNGears.prototype.addWeekToTimesheet = function(new_date) {
    // determine the date of the previous monday
    var monday_date = new Date(new_date.toString());
    monday_date.setDate(monday_date.getDate() - monday_date.getDay() + 1);

    // determine the position for the new week - ordered going down by date descending
    var monday_date_formatted = monday_date.getFullYear().toString() + '-' + this.zeroPad((monday_date.getMonth()+1).toString(),2) + '-' + this.zeroPad(monday_date.getDate().toString(),2);

    // If already exists, reject
    if ($('#' + monday_date_formatted).length > 0) {
        alert('Please choose a week that hasn\'t been created yet');
        return;
    }

    var insertAfter = null;
    // return false to break the each loop
    $('div.week').not('#weekTemplate').each(function(){
        if (monday_date_formatted < this.id) {
            insertAfter = $(this);
        }
        if (monday_date_formatted === this.id) {
            return false;
        }
        if (monday_date_formatted > this.id) {
            return false;
        }
    });

    // clone the week template and insert
    var newWeek = $('div#weekTemplate').clone(true);
    if (insertAfter !== null) {
        newWeek.insertAfter(insertAfter).slideDown();
    } else {
        newWeek.prependTo('div#timesheet').slideDown();
    }

    // fill out the week template
    newWeek.attr('id', monday_date_formatted);
    var friday_date = new Date(new_date.toString());
    friday_date.setDate(friday_date.getDate() + (7 - friday_date.getDay()));
    newWeek.find('td.dateRangeCell').text(monday_date.getDate() + '/' + (monday_date.getMonth() + 1) + '/' + monday_date.getFullYear() + ' - ' + friday_date.getDate() + '/' + (friday_date.getMonth() + 1) + '/' + friday_date.getFullYear());
    newWeek.find('th span.date').each(function(){
        var day_of_month = monday_date.getDate();
        var month = monday_date.getMonth() + 1 ;
        this.innerHTML = day_of_month + '/' + month;
        monday_date.setDate(day_of_month + 1);
    });
};

TimeNGears.prototype.deleteRow = function(btn) {
    if (confirm('Are you sure?')) {
        var parentTr = $(btn).parents('tr').eq(0);
        var id = parentTr.attr('id');
        if (id !== '') {
            this.db.execute('delete from entry where entry_id = ?', [id]);
        }
        var week = $(btn).parents('div.week').eq(0);
        parentTr.remove();
        this.totalWeek(week.attr('id'));
    }
};

TimeNGears.prototype.openIssueTracker = function(value) {
    var rs = this.db.execute("select issue_tracker_url from settings");
    var url = rs.field(0);
    if (url !== '' && url !== null) {
        url += "/ticket/" + value;
        window.open(url, "ticket_" + value);
    }
}

// week: id
// row: index
// day: day name
TimeNGears.prototype.focus = function(week, row, day) {
    $('.week#' + week + ' .timesheetRow, .week#' + week + ' .template').eq(row).find(' .' + day).click();
}

TimeNGears.prototype.whichDay = function(classes) {
    return this.days_of_week.intersection(classes.split(' '))[0];
};

TimeNGears.prototype.whichCell = function(classes) {
    return this.input_cells.intersection(classes.split(' '))[0];
}

TimeNGears.prototype.nextCell = function(day) {

    switch (day) {
        case 'tracCell': return 'jobCell';
        case 'jobCell': return 'monday';
        case 'monday': return 'tuesday';
        case 'tuesday': return 'wednesday';
        case 'wednesday': return 'thursday';
        case 'thursday': return 'friday';
        case 'friday': return 'saturday';
        case 'saturday': return 'sunday';
        case 'sunday': return 'tracCell';
        default: throw 'Error';
    }
};

TimeNGears.prototype.previousCell = function(day) {

    switch (day) {
        case 'tracCell': return 'sunday';
        case 'jobCell': return 'tracCell';
        case 'monday': return 'jobCell';
        case 'tuesday': return 'monday';
        case 'wednesday': return 'tuesday';
        case 'thursday': return 'wednesday';
        case 'friday': return 'thursday';
        case 'saturday': return 'friday';
        case 'sunday': return 'saturday';
        default: throw 'Error';
    }
}

TimeNGears.prototype.zeroPad = function(n, digits) {
    n = n.toString();
    while (n.length < digits) {
        n = '0' + n;
    }
    return n;
};

// Calculate the column totals and then the grand total given the id of a
// certain week (a date string of the monday)
TimeNGears.prototype.totalWeek = function(week) {
    for (var i in this.days_of_week) {
        // if is numeric, and not 'intersection'
        if (parseInt(i) == i) {
            this.totalColumn(week, this.days_of_week[i]);
        }
    }
    this.totalGrand(week);
};

TimeNGears.prototype.runCalculation = function(cell) {

    // calculate horizontal total
    var tr = cell.parents('tr').eq(0);
    this.totalRow(tr);

    var week = cell.parents('div.week').eq(0).attr('id');
    var day_of_week = this.whichDay(cell.attr('class'));

    // calculate vertical total
    this.totalColumn(week, day_of_week);

    // calculate grand total - running across the top
    this.totalGrand(week);
};

// find the total hours for a given row (tr)
TimeNGears.prototype.totalRow = function(row) {

    var total = 0;
    row.find('td.timeCell').filter('td.inputCell').find('span.value').each(function(){
        // chrome returns a real nbsp 160?
        if (this.innerHTML !== '' && this.innerHTML !== '&nbsp;' && this.innerHTML.charCodeAt(0) !== 160) {
            total += parseFloat(this.innerHTML);
        }
    });
    row.find('td.totalCell span.value').text(total);
};

// find the total hours for a given column (using a day classname) and week
TimeNGears.prototype.totalColumn = function(week, day) {

    var total = 0;
    $('div#'+week+' td.'+day+' span.value').each(function(){
        // chrome returns a real nbsp 160?
        if (this.innerHTML !== '' && this.innerHTML !== '&nbsp;' && this.innerHTML.charCodeAt(0) !== 160) {
            total += parseFloat(this.innerHTML);
        }
    });
    // for vertical totals, don't display a zero - columns may be blank, rows will most likely not be
    if (total === 0) {
        // doing text(' ') will not tranlsate into nbsp =/  - at least for chrome anyway
        $('div[id="'+week+'"] td.'+day).filter('td.totalCell').html('&nbsp;');
    } else {
        $('div[id="'+week+'"] td.'+day).filter('td.totalCell').text(total);
    }
};

// find the grand total by running along the column totals
TimeNGears.prototype.totalGrand = function(week) {
    var total = 0;
    $('div#'+week+' td.'+'topTotal').each(function(){
        // chrome returns a real nbsp 160?
        if (this.innerHTML !== '' && this.innerHTML !== '&nbsp;' && this.innerHTML.charCodeAt(0) !== 160) {
            total += parseFloat(this.innerHTML);
        }
    });
    $('div#'+week+' td.grandTotal').text(total);
};

TimeNGears.prototype.showNotification = function(msg) {
    var notification = $('div#notification').text(msg).fadeIn(this.notification_fadein_delay);
    window.setTimeout(function(){
        notification.fadeOut(this.notification_fadeout_delay);
    }, this.notification_display_delay);
}

TimeNGears.prototype.setSaved = function(is_saved) {
    this.is_saved = is_saved;
    if (is_saved) {
        $('#unsaved').hide();
        $('button#save').attr('disabled', 'disabled');
        $('button#discard').attr('disabled', 'disabled');
    } else {
        $('#unsaved').show();
        $('button#save').removeAttr('disabled');
        $('button#discard').removeAttr('disabled');
    }
};

TimeNGears.prototype.loadTimesheet = function(timesheet_id) {

    // find out the timesheet name
    // (maybe just pass the name from the ui?)
    var rs = this.db.execute('select name from timesheet where timesheet_id = ?', [timesheet_id]);
    var timesheet_name = rs.field(0);
    rs.close();

    // update screen
    $('div#timesheet').empty();
    $('#timesheet_name').text(timesheet_name);
    $('#timesheet_id').val(timesheet_id);

    // load the timesheet
    var rs = this.db.execute('select * from entry where timesheet_id = ? order by monday_date desc, entry_id', [timesheet_id]);
    var current_week = -1;
    var week_template = $("#weekTemplate");
    while (rs.isValidRow()) {

        var db_monday_date = rs.fieldByName('monday_date');

        // create a new table for each week
        if (current_week != db_monday_date) {

            // if the current window is filled
            if ($(document).height() > $(window).height()) {
                break;
            }

            if (db_monday_date !== null) {
                var monday_date_pieces = db_monday_date.split('-');
                var monday_date = new Date(monday_date_pieces[0], monday_date_pieces[1] - 1, monday_date_pieces[2]);
            } else {
                var monday_date = new Date;
                monday_date.setDate(monday_date.getDate() - monday_date.getDay() + 1);
            }
            current_week = this.zeroPad(monday_date.getFullYear().toString(),2) + '-' + this.zeroPad((monday_date.getMonth()+1).toString(),2) + '-' + this.zeroPad(monday_date.getDate().toString(),2);

            var newWeek = week_template.clone(true);
            newWeek.attr('id', current_week);

            var friday_date = new Date(monday_date);
            friday_date.setDate(friday_date.getDate() + (7 - friday_date.getDay()));
            newWeek.find('td.dateRangeCell').text(monday_date.getDate() + '/' + (monday_date.getMonth() + 1) + '/' + monday_date.getFullYear() + ' - ' + friday_date.getDate() + '/' + (friday_date.getMonth() + 1) + '/' + friday_date.getFullYear());

            newWeek.find('th span.date').each(function(){
                var day_of_month = monday_date.getDate();
                var month = monday_date.getMonth() + 1 ;
                this.innerHTML = day_of_month + '/' + month;
                monday_date.setDate(day_of_month + 1);
            });

            newWeek.appendTo('div#timesheet').show();
        }

        var template = $('div#' + current_week + ' tr.template');
        var newRow = template.clone(true);
        newRow.removeClass('template');
        for (var i in this.db_fields) {
            var value = rs.fieldByName(i);
            if (i === 'category') {
                if (value !== '') {
                    newRow.find('td.'+this.db_fields[i]+' .value').val(value);
                    newRow.find('td.'+this.db_fields[i]+' .display').text(newRow.find('select[name="job_category"] option[value="'+value+'"]').text());
                }
            } else {
                if (value === '') {
                    newRow.find('td.'+this.db_fields[i]+' span.value').html('&nbsp;');
                } else {
                    newRow.find('td.'+this.db_fields[i]+' span.value').text(value);
                }
            }
        }

        newRow.attr('id', rs.fieldByName('entry_id'));
        newRow.addClass('timesheetRow').find('input.delete').show();
        newRow.insertBefore(template).show();
        this.totalRow(newRow);
        rs.next();

    }
    rs.close();

    var that = this;
    $('div.week').each(function(){
        that.totalWeek($(this).attr('id'));
    });
};

TimeNGears.prototype.loadMore = function() {

    var final_week = $('.week:last');
    var date = final_week.attr("id");
    var week_template = $("#weekTemplate");
    var timesheet_id = $('#timesheet_id').val();

    var rs = this.db.execute("select * from entry where timesheet_id = ? and monday_date < ? order by monday_date desc, entry_id", [timesheet_id, date]);
    var current_week = null;
    while (rs.isValidRow()) {

        var db_monday_date = rs.fieldByName("monday_date");
        var monday_date_pieces = db_monday_date.split('-');
        var monday_date = new Date(monday_date_pieces[0], monday_date_pieces[1] - 1, monday_date_pieces[2]);

        if (current_week == null) {
            current_week = this.zeroPad(monday_date.getFullYear().toString(),2) + '-' + this.zeroPad((monday_date.getMonth()+1).toString(),2) + '-' + this.zeroPad(monday_date.getDate().toString(),2);

            var newWeek = week_template.clone(true);
            newWeek.attr('id', current_week);

            var friday_date = new Date(monday_date);
            friday_date.setDate(friday_date.getDate() + (7 - friday_date.getDay()));
            newWeek.find('td.dateRangeCell').text(monday_date.getDate() + '/' + (monday_date.getMonth() + 1) + '/' + monday_date.getFullYear() + ' - ' + friday_date.getDate() + '/' + (friday_date.getMonth() + 1) + '/' + friday_date.getFullYear());

            newWeek.find('th span.date').each(function(){
                var day_of_month = monday_date.getDate();
                var month = monday_date.getMonth() + 1 ;
                this.innerHTML = day_of_month + '/' + month;
                monday_date.setDate(day_of_month + 1);
            });

            newWeek.appendTo('div#timesheet').show();
        }

        if (current_week != db_monday_date) {
            break;
        }

        var template = $('div#' + current_week + ' tr.template');
        var newRow = template.clone(true);
        newRow.removeClass('template');
        for (var i in this.db_fields) {
            var value = rs.fieldByName(i);
            if (i === 'category') {
                if (value !== '') {
                    newRow.find('td.'+this.db_fields[i]+' .value').val(value);
                    newRow.find('td.'+this.db_fields[i]+' .display').text(newRow.find('select[name="job_category"] option[value="'+value+'"]'));
                }
            } else {
                if (value === '') {
                    newRow.find('td.'+this.db_fields[i]+' span.value').html('&nbsp;');
                } else {
                    newRow.find('td.'+this.db_fields[i]+' span.value').text(value);
                }
            }
        }

        newRow.attr('id', rs.fieldByName('entry_id'));
        newRow.addClass('timesheetRow').find('input.delete').show();
        newRow.insertBefore(template).show();
        this.totalRow(newRow);

        rs.next();
    }
    rs.close();

    return current_week != null;
};

